﻿using System;
using System.Collections.Generic;
using System.Reflection;

namespace YourLastOptionsDialog.Core.Attributes
{
    /// <summary>
    /// An attribute used to decorate properties of arbitrary classes to configure their treatment as options.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property)]
    public abstract class OptionAttribute : Attribute
    {
        private readonly Dictionary<string, Func<string>> _propertyAccessors = new Dictionary<string, Func<string>>();

        /// <summary>
        /// Gets or sets the display name.
        /// </summary>
        public string DisplayName
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the description.
        /// </summary>
        public string Description
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the name of the group that is used to group multiple options in the UI.
        /// </summary>
        public string GroupName
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the display order that determines the order of options within a group.
        /// </summary>
        public int DisplayOrder
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the type of the resource used to retrieve (localized) values for other properties
        /// like <see cref="DisplayName"/> and <see cref="Description"/>. If this is set, 
        /// the values of these properties are treated as lookup keys for the actual values.
        /// </summary>
        public Type ResourceType
        {
            get;
            set;
        }

        /// <summary>
        /// Formats the specified property value, either by returning a localized resource for the given
        /// value, or by returning the value itself if no localized resources have been configured.
        /// </summary>
        /// <param name="propertyValueOrKey">The property value that may or may not be a resource key.</param>
        /// <returns>A localized resource for the given value, or the value itself if no localized resources have been configured.</returns>
        public string Format(string propertyValueOrKey)
        {
            if (string.IsNullOrEmpty(propertyValueOrKey))
            {
                return propertyValueOrKey;
            }

            // get the accessor
            var accessor = GetPropertyAccessor(propertyValueOrKey);
            var result = accessor();
            return result;
        }

        /// <summary>
        /// Gets an accessor to the actual value of any property of the option attribute.
        /// This works as follows: if the user has defined a <see cref="ResourceType"/>,
        /// this method reflects on the resource to get an accessor that can be used to 
        /// fetch the actual value. If no <see cref="ResourceType"/> has been configured
        /// or the property value could not be found in the resources as key, the returned
        /// accessor passes through the property value itself.
        /// </summary>
        /// <param name="propertyValueOrKey">The property value that also is a potential key
        /// in a resource class if the <see cref="ResourceType"/> has been configured.</param>
        /// <returns>An accessor that returns either the value from a resource class, 
        /// or simply the unchanged property value for the given property.</returns>
        protected Func<string> GetPropertyAccessor(string propertyValueOrKey)
        {
            // we cache accessors so we only have to build them once
            // this is possible because the attribute settings won't change at runtime
            if (_propertyAccessors.ContainsKey(propertyValueOrKey))
            {
                return _propertyAccessors[propertyValueOrKey];
            }

            Func<string> result = null;

            // do we have a resource type?
            if (ResourceType != null)
            {
                // this unfortunately only works for public properties,
                // so the user must choose "Public" as modifier for their resource files
                var resourceProperty = ResourceType.GetProperty(propertyValueOrKey, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
                if (resourceProperty != null && resourceProperty.GetGetMethod() != null && resourceProperty.PropertyType == typeof(string))
                {
                    result = () => (string)resourceProperty.GetValue(null, null);
                }
            }

            // no resource type? => pass through the original value
            if (result == null)
            {
                result = () => propertyValueOrKey;
            }

            // add for reuse
            _propertyAccessors.Add(propertyValueOrKey, result);
            return result;
        }
    }
}
